﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ZackFlame.SimpleBrainFuck.Core.Statments;
using ZackFlame.SimpleBrainFuck.TextResources;

namespace ZackFlame.SimpleBrainFuck.Core
{
    public sealed class SimpleTranslator
    {
        readonly List<BrainDef> SystemDefs = new List<BrainDef>();
        readonly BrainMacros[] SystemMacroses;

        public SimpleTranslator()
        {
            SystemDef("error", ms => SystemFunctions.Error(ms[0]), "text");
            SystemDef("shift", ms => SystemFunctions.Shift(int.Parse(ms[0])), "k");
            SystemDef("zero", ms => SystemFunctions.Zero());
            SystemDef("add", ms => SystemFunctions.Add(int.Parse(ms[0])), "k");
            SystemDef("sub", ms => SystemFunctions.Sub(int.Parse(ms[0])), "k");
            SystemDef("move", ms => SystemFunctions.Move(int.Parse(ms[0])), "k");
            SystemDef("copy", ms => SystemFunctions.Copy(int.Parse(ms[0]), int.Parse(ms[1])), "k", "t");
            SystemDef("repeat", ms => SystemFunctions.Repeat(ms[0], int.Parse(ms[1])), "text", "k");
            SystemDef("value", ms => SystemFunctions.Value(int.Parse(ms[0]), int.Parse(ms[1])), "k", "t");
            SystemDef("text", ms => SystemFunctions.Text(ms[0]), "text");
            SystemDef("type", ms => SystemFunctions.Type(ms[0], int.Parse(ms[1])), "text", "t");
            SystemDef("print", ms => SystemFunctions.Print(int.Parse(ms[0])), "k");

            SystemMacroses = new BrainMacros[]
                {
                    new BrainMacros("__version__")
                    {
                        Value = typeof(SimpleParser).Assembly.GetName().Version.Major.ToString()
                    },
                    new BrainMacros("__platform__")
                    { 
                        Value = Environment.OSVersion.Platform.ToString()
                    },
                };
        }

        private void SystemDef(string name,
            Func<string[], string> function, params string[] macrosesNames)
        {
            SystemDefs.Add(new BrainDef(function)
            {
                Name = name,
                MacrosArgs = macrosesNames
                    .Select(macrosName => new BrainMacros() { Name = macrosName })
                    .ToList()
            });
        }

        public string RenderBrainFuck(IEnumerable<Statment> semanticTree)
        {
            Stack<BrainDef> callStack = new Stack<BrainDef>();
            return RenderTree(semanticTree, callStack, SystemMacroses, SystemDefs);
        }

        private string RenderTree(
            IEnumerable<Statment> semanticTree, Stack<BrainDef> callStack, 
            IEnumerable<BrainMacros> macroses, IEnumerable<BrainDef> defs)
        {
            var currentDefs = new List<BrainDef>();
            var currentMacroses = new List<BrainMacros>();
            StringBuilder brainString = new StringBuilder();

            foreach (Statment statment in semanticTree)
            {
                if (statment is BrainDef)
                {
                    BrainDef defStat = (BrainDef)statment;
                    ProcessDef(defStat, currentDefs);
                }
                else if (statment is BrainIf)
                {
                    BrainIf ifStat = (BrainIf)statment;
                    var allMacroses = macroses.Concat(currentMacroses);
                    var allDefs = defs.Concat(currentDefs);
                    brainString.Append(RenderIf(ifStat, callStack, allMacroses, allDefs));
                }
                else if (statment is BrainCall)
                {
                    BrainCall callStat = (BrainCall)statment;

                    var allMacroses = macroses.Concat(currentMacroses);
                    var allDefs = defs.Concat(currentDefs);

                    BrainDef def = allDefs.LastOrDefault(d => d.Name == callStat.DefName);
                    if (def == null)
                        throw new CodeErrorException(callStat.Line, CodeErrors.Call_DefNotFound);
                    if (callStack.Contains(def))
                        throw new CodeErrorException(callStat.Line, CodeErrors.Call_CycleCall);

                    if (callStat.CallArgs.Count != def.MacrosArgs.Count)
                        throw new CodeErrorException(callStat.Line, CodeErrors.Call_InvalidCallArgsCount);

                    brainString.Append(RenderCall(callStat, def, callStack, allMacroses, allDefs));
                }
                else if (statment is BrainMacrosAssignment)
                {
                    var assignment = (BrainMacrosAssignment)statment;
                    ProcessAssignment(assignment, macroses, currentMacroses);
                }
                else if (statment is BrainPureCode)
                {
                    brainString.Append(((BrainPureCode)statment).Value);
                }

                brainString.AppendLineLF();
            }

            return brainString.ToString();
        }

        private void ProcessDef(BrainDef defStat, List<BrainDef> currentDefs)
        {
            if (!BrainDef.NameRegex.Match(defStat.Name).Success)
                throw new CodeErrorException(defStat.Line, CodeErrors.Def_InvalidName);

            if (currentDefs.Exists(def => def.Name == defStat.Name))
                throw new CodeErrorException(defStat.Line, CodeErrors.Def_DefWithNameAlreadyExists);

            var matches = from macro in defStat.MacrosArgs
                          select BrainMacros.NameRegex.Match(macro.Name);

            if (!matches.All(m => m.Success))
                throw new CodeErrorException(defStat.Line, CodeErrors.Def_InvalidParameterName);
            if (defStat.MacrosArgs.Any(m => m.Negative))
                throw new CodeErrorException(defStat.Line, CodeErrors.Def_ParameterCantBeNegative);

            for (int i = 0; i < defStat.MacrosArgs.Count; i++)
			{
                var parameter = defStat.MacrosArgs[i];
                for (int j = i + 1; j < defStat.MacrosArgs.Count; j++)
                {
                    if (parameter.Name == defStat.MacrosArgs[j].Name)
                        throw new CodeErrorException(defStat.Line, CodeErrors.Def_ParameterAlreadyDefined);
                }
			}            

            currentDefs.Add(defStat);
        }

        private string RenderIf(BrainIf ifStatment, Stack<BrainDef> callStack,
            IEnumerable<BrainMacros> macroses, IEnumerable<BrainDef> defs)
        {
            string parameter = CalculateArgument(ifStatment.Parameter, macroses);
            if (parameter == null)
                throw new CodeErrorException(ifStatment.Line, CodeErrors.Call_MacrosNotDefined);

            int tParam;
            if (!int.TryParse(parameter, out tParam))
                throw new CodeErrorException(ifStatment.Line, CodeErrors.Call_InvalidArgumentsFormat);

            StringBuilder result = new StringBuilder()
                .AppendLineLF(SystemFunctions.If(tParam))
                .Append(RenderTree(ifStatment.IfStatments, callStack, macroses, defs))
                .AppendLineLF(SystemFunctions.Else(tParam))
                .Append(RenderTree(ifStatment.ElseStatments, callStack, macroses, defs))
                .AppendLineLF(SystemFunctions.EndIf(tParam));

            return result.ToString();
        }

        private string RenderCall(BrainCall callStatment, 
            BrainDef callTarget, Stack<BrainDef> callStack,
            IEnumerable<BrainMacros> macroses, IEnumerable<BrainDef> defs)
        {
            var callParameters = callStatment.CallArgs.Select(arg =>
            {
                string value = CalculateArgument(arg, macroses);
                if (value == null)
                    throw new CodeErrorException(callStatment.Line, CodeErrors.Call_MacrosNotDefined);
                return value;
            });

            if (callTarget.CanRenderItself)
            {
                try
                {
                    return callTarget.Render(callParameters.ToArray());
                }
                catch (FormatException)
                {
                    throw new CodeErrorException(callStatment.Line, CodeErrors.Call_InvalidArgumentsFormat);
                }
                catch (ArgumentException ex)
                {
                    throw new CodeErrorException(callStatment.Line, ex.Message);
                }
                catch (CodeErrorException ex)
                {
                    ex.Line = callStatment.Line;
                    throw ex;
                }
            }
            else
            {
                var definedMacroses = callTarget.MacrosArgs.Zip(callParameters,
                    (m, p) => new BrainMacros() { Name = m.Name, Value = p });
                var allMacroses = macroses.Concat(definedMacroses);

                callStack.Push(callTarget);
                string rendered = RenderTree(callTarget.Statments, callStack, allMacroses, defs);
                callStack.Pop();
                return rendered;
            }
        }

        private void ProcessAssignment(BrainMacrosAssignment assignment,
            IEnumerable<BrainMacros> macroses, List<BrainMacros> currentMacroses)
        {
            if (!BrainMacros.NameRegex.Match(assignment.Name).Success)
                throw new CodeErrorException(assignment.Line, CodeErrors.Macro_InvalidName);

            var allMacroses = macroses.Concat(currentMacroses);

            string leftValue = CalculateArgument(assignment.Expression.Left, allMacroses);
            if (leftValue == null)
                throw new CodeErrorException(assignment.Line, CodeErrors.Call_MacrosNotDefined);

            string rightValue = CalculateArgument(assignment.Expression.Right, allMacroses);
            string result = null;

            string oper = assignment.Expression.Operator;
            if (oper == null)
            {
                result = leftValue;
            }
            else if (oper == "-" && assignment.Expression.Right == null)
            {
                int left;
                if (int.TryParse(leftValue, out left))
                {
                    result = (-left).ToString();
                }
                else
                {
                    throw new CodeErrorException(assignment.Line,
                        CodeErrors.Assignment_InvalidArgumentsFormat);
                }
            }
            else if (oper == "+" || oper == "-" || oper == "*" || oper == "/"
                && assignment.Expression.Right != null)
            {
                if (rightValue == null)
                    throw new CodeErrorException(assignment.Line, CodeErrors.Call_MacrosNotDefined);

                int left;
                int right;

                if (int.TryParse(leftValue, out left) &&
                    int.TryParse(rightValue, out right))
                {
                    int resultValue = 0;
                    if (oper == "+")
                        resultValue = left + right;
                    else if (oper == "-")
                        resultValue = left - right;
                    else if (oper == "*")
                        resultValue = left * right;
                    else if (oper == "/")
                        resultValue = left / right;

                    result = resultValue.ToString();
                }
                else
                {
                    throw new CodeErrorException(assignment.Line,
                        CodeErrors.Assignment_InvalidArgumentsFormat);
                }
            }
            else if (oper == "." && assignment.Expression.Right != null)
            {
                if (rightValue == null)
                    throw new CodeErrorException(assignment.Line, CodeErrors.Call_MacrosNotDefined);

                result = string.Concat(leftValue, rightValue);
            }
            else if (oper == "length" && assignment.Expression.Right == null)
            {
                result = leftValue.Length.ToString();
            }
            else if (oper == "take" || oper == "skip" && assignment.Expression.Right != null)
            {
                if (rightValue == null)
                    throw new CodeErrorException(assignment.Line, CodeErrors.Call_MacrosNotDefined);

                int right;
                if (int.TryParse(rightValue, out right))
                {
                    if (right < 0 || right > leftValue.Length)
                    {
                        throw new CodeErrorException(assignment.Line,
                            CodeErrors.Operator_InvalidTakeOrSkipCount);
                    }

                    if (oper == "take")
                        result = leftValue.Substring(0, right);
                    else
                        result = leftValue.Substring(right);
                }
            }
            else
            {
                throw new CodeErrorException(assignment.Line, CodeErrors.Assignment_InvalidOperator);
            }

            BrainMacros current = currentMacroses.FirstOrDefault(
                m => m.Name == assignment.Name);

            if (current != null)
            {
                current.Value = result;
            }
            else
            {
                currentMacroses.Add(new BrainMacros()
                {
                    Name = assignment.Name,
                    Value = result,
                });
            }
        }

        private string CalculateArgument(BrainArg argument, IEnumerable<BrainMacros> macroses)
        {
            if (argument == null)
                return null;

            BrainMacros macros = argument as BrainMacros;
            if (macros != null)
            {
                BrainMacros definition = macroses
                    .LastOrDefault(m => m.Name == macros.Name);

                if (definition == null)
                {
                    return null;
                }
                else
                {
                    if (macros.Negative)
                    {
                        int value;
                        if (!int.TryParse(definition.Value, out value))
                            throw new CodeErrorException(argument.Line, CodeErrors.Macro_NotANumber);

                        return (-value).ToString();
                    }
                    else
                    {
                        return definition.Value;
                    }
                }
            }
            else
            {
                return argument.Value;
            }
        }
    }
}
